---
id: cache
title: 14. 分布式缓存
sidebar_label: 14. 分布式缓存
---

## 14.1 什么是缓存

缓存可以减少生成内容所需的工作，从而显著提高应用程序的性能和可伸缩性。 **缓存最适用于不经常更改的数据，因为生成成本很高**。 通过缓存，可以比从数据源返回的数据的副本速度快得多。 应该对应用进行编写和测试，使其永不依赖于缓存的数据。

## 14.2 缓存类型

- 内存缓存：顾名思义，就是缓存在应用部署所在服务器的内存中
- 分布式缓存：分布式缓存是由多个应用服务器共享的缓存
- 响应缓存：缓存服务器端 `Not Modified` 的数据

## 14.3 内存缓存使用

内存缓存是最常用的缓存方式，具有存取快，效率高特点。

内存缓存通过注入 `IMemoryCache` 方式注入即可。

:::important 备注

在 `Furion` 框架中，内存缓存服务已经默认注册，无需手动注册。

:::

### 14.3.1 基本使用

如，缓存当前时间：

```cs {13,21-24}
using Furion.DynamicApiController;
using Microsoft.Extensions.Caching.Memory;
using System;

namespace Furion.Application
{
    public class CacheServices : IDynamicApiController
    {
        private const string _timeCacheKey = "cache_time";

        private readonly IMemoryCache _memoryCache;

        public CacheServices(IMemoryCache memoryCache)
        {
            _memoryCache = memoryCache;
        }

        [ApiDescriptionSettings(KeepName = true)]
        public DateTimeOffset GetOrCreate()
        {
            return _memoryCache.GetOrCreate(_timeCacheKey, entry =>
            {
                return DateTimeOffset.UtcNow;
            });
        }
    }
}
```

### 14.3.2 设置缓存选项

内存缓存支持设置缓存时间、缓存大小、及绝对缓存过期时间等

```cs
_memoryCache.GetOrCreate(_timeCacheKey, entry =>
{
    entry.SlidingExpiration = TimeSpan.FromSeconds(3);  // 滑动缓存时间
    return DateTimeOffset.UtcNow;
});

await _memoryCache.GetOrCreateAsync(_timeCacheKey, async entry =>
{
    // 这里可以使用异步~~
});
```

:::caution 关于缓存时间

只有具有可调过期的缓存项集存在过时的风险。 如果访问的时间比滑动过期时间间隔更频繁，则该项将永不过期。

将弹性过期与绝对过期组合在一起，以保证项目在其绝对过期时间通过后过期。 绝对过期会将项的上限设置为可缓存项的时间，同时仍允许项在可调整过期时间间隔内未请求时提前过期。

如果同时指定了绝对过期和可调过期时间，则过期时间以逻辑方式运算。 如果滑动过期时间间隔 或 绝对过期时间通过，则从缓存中逐出该项。

如：

```cs
_memoryCache.GetOrCreate(_timeCacheKey, entry =>
{
    entry.SetSlidingExpiration(TimeSpan.FromSeconds(3));
    entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromSeconds(20);
    return DateTime.Now;
});
```

前面的代码保证数据的缓存时间不超过绝对时间。

:::

### 14.3.3 手动设置缓存选项

除了上面的 `Func<MemoryCacheEntryOptions, object>` 方式设置缓存选项，我们可以手动创建并设置，如：

```cs
var cacheEntryOptions = new MemoryCacheEntryOptions()
                .SetSlidingExpiration(TimeSpan.FromSeconds(3));

_memoryCache.Set(_timeCacheKey, DateTimeOffset.UtcNow, cacheEntryOptions);
```

### 14.3.4 缓存依赖关系

下面的示例演示如何在依赖条目过期时使缓存条目过期。 `CancellationChangeToken` 添加到缓存的项。 当 `Cancel` 在上调用时 `CancellationTokenSource` ，将逐出两个缓存项。

```cs
public IActionResult CreateDependentEntries()
{
    var cts = new CancellationTokenSource();
    _cache.Set(CacheKeys.DependentCTS, cts);

    using (var entry = _cache.CreateEntry(CacheKeys.Parent))
    {
        // expire this entry if the dependant entry expires.
        entry.Value = DateTime.Now;
        entry.RegisterPostEvictionCallback(DependentEvictionCallback, this);

        _cache.Set(CacheKeys.Child,
            DateTime.Now,
            new CancellationChangeToken(cts.Token));
    }

    return RedirectToAction("GetDependentEntries");
}

public IActionResult GetDependentEntries()
{
    return View("Dependent", new DependentViewModel
    {
        ParentCachedTime = _cache.Get<DateTime?>(CacheKeys.Parent),
        ChildCachedTime = _cache.Get<DateTime?>(CacheKeys.Child),
        Message = _cache.Get<string>(CacheKeys.DependentMessage)
    });
}

public IActionResult RemoveChildEntry()
{
    _cache.Get<CancellationTokenSource>(CacheKeys.DependentCTS).Cancel();
    return RedirectToAction("GetDependentEntries");
}

private static void DependentEvictionCallback(object key, object value,
    EvictionReason reason, object state)
{
    var message = $"Parent entry was evicted. Reason: {reason}.";
    ((HomeController)state)._cache.Set(CacheKeys.DependentMessage, message);
}
```

使用 `CancellationTokenSource` 允许将多个缓存条目作为一个组逐出。 `using` 在上面的代码中，在块中创建的缓存条目 `using` 将继承触发器和过期设置。

:::note 了解更多

想了解更多 `内存中的缓存` 知识可查阅 [ASP.NET Core - 内存缓存](https://docs.microsoft.com/zh-cn/aspnet/core/performance/caching/memory?view=aspnetcore-5.0) 章节。

:::

## 14.4 分布式缓存

分布式缓存是由多个应用服务器共享的缓存，通常作为外部服务在访问它的应用服务器上维护。 分布式缓存可以提高 `ASP.NET Core` 应用程序的性能和可伸缩性，尤其是在应用程序由云服务或服务器场托管时。

与其他缓存方案相比，分布式缓存具有多项优势，其中缓存的数据存储在单个应用服务器上。

当分布式缓存数据时，数据将：

- (一致性) 跨多个 服务器的请求
- 存活在服务器重启和应用部署之间
- 不使用本地内存

分布式缓存配置是特定于实现的。 本文介绍如何配置 `SQL Server` 和 `Redis` 分布式缓存。 第三方实现也可用，例如 GitHub 上的 [NCache](https://github.com/Alachisoft/NCache) (NCache) 。

**无论选择哪种实现，应用都会使用接口与缓存交互 `IDistributedCache` 。**

### 14.4.1 使用条件

- 若要使用 `SQL Server` 分布式缓存，则添加 `Microsoft.Extensions.Caching.SqlServer` 包
- 若要使用 `Redis` 分布式缓存，则添加 `Microsoft.Extensions.Caching.StackExchangeRedis` 包
- 若要使用 `NCache` 分布式缓存，则添加 `NCache.Microsoft.Extensions.Caching.OpenSource` 包

### 14.4.2 `IDistributedCache`

`IDistributedCache` 接口提供以下方法来处理分布式缓存实现中的项：

- `Get/GetAsync`：接受字符串键，并检索缓存项作为 `byte[]` 数组（如果在缓存中找到）
- `Set/SetAsync`：使用字符串键将项 (作为 `byte[]` 数组) 添加到缓存中
- `Refresh/RefreshAsync` ：根据项的键刷新缓存中的项，重置其滑动过期超时（如果有）
- `Remove/RemoveAsync`：根据缓存项的字符串键删除缓存项

### 14.4.3 分布式内存缓存

分布式内存缓存（`AddDistributedMemoryCache`）是一个框架提供的实现 `IDistributedCache` ，它将项存储在内存中。 **分布式内存缓存不是实际的分布式缓存，缓存项由应用程序实例存储在运行应用程序的服务器上。**

分布式内存缓存优点：

- 用于开发和测试方案。
- 在生产环境中使用单一服务器并且内存消耗不是问题。 实现分布式内存缓存会抽象化缓存的数据存储。 如果需要多个节点或容错，可以在将来实现真正的分布式缓存解决方案。

:::important 备注

在 `Furion` 框架中，分布式内存缓存服务已经默认注册，无需手动调用 `services.AddDistributedMemoryCache();` 注册。

:::

### 14.4.4 分布式 SQL Server 缓存

分布式 `SQL Server` 缓存实现 (`AddDistributedSqlServerCache`) 允许分布式缓存使用 `SQL Server` 数据库作为其后备存储。

若要在 `SQL Server` 实例中创建 `SQL Server` 缓存的项表，可以使用 `sql-cache` 工具。 该工具将创建一个表，其中包含指定的名称和架构。

通过运行命令 `sql-cache create` 创建一个表，提供 `SQL Server` 实例 (Data Source) 、数据库 (Initial Catalog) 、架构 (例如) dbo 和表名称。例如 `TestCache`：

```shell
dotnet sql-cache create "Data Source=(localdb)\MSSQLLocalDB;Initial Catalog=DistCache;Integrated Security=True;" dbo TestCache
```

创建成功后，在 `Startup.cs` 中注册即可：

```cs
services.AddDistributedSqlServerCache(options =>
{
    options.ConnectionString =
        _config["DistCache_ConnectionString"];
    options.SchemaName = "dbo";
    options.TableName = "TestCache";
});
```

### 14.4.5 分布式 `Redis` 缓存

`Redis` 是内存中数据存储的开源数据存储，通常用作分布式缓存。在使用时通过 `services.AddStackExchangeRedisCache()` 中注册即可。

这里不细讲 `Redis` 相关内容，后续章节会使用基本例子演示。

`Redis` 基本配置：

```cs
services.AddStackExchangeRedisCache(options =>
{
    // 连接字符串，这里也可以读取配置文件
    options.Configuration = "192.168.111.134,password=aW1HAyupRKmiZn3Q";
    // 键名前缀
    options.InstanceName = "furion_";
});
```

### 14.4.6 分布式 `NCache` 缓存

`NCache` 是在 `.NET` 和 `.Net Core` 中以本机方式开发的开源内存中分布式缓存。 `NCache` 在本地工作并配置为分布式缓存群集，适用于在 `Azure` 或其他托管平台上运行的 `ASP.NET Core` 应用。
若要在本地计算机上安装和配置 `NCache`，请参阅 [适用于 Windows 的 NCache 入门指南](https://www.alachisoft.com/resources/docs/ncache-oss/getting-started-guide-windows/)。

`NCache` 基本配置：

- 安装 `Alachisoft.NCache.OpenSource.SDK` 包
- 在 [ncconf](https://www.alachisoft.com/resources/docs/ncache-oss/admin-guide/client-config.html) 中配置缓存群集
- 注册 `NCache` 服务

```cs
services.AddNCacheDistributedCache(configuration =>
{
    configuration.CacheName = "demoClusteredCache";
    configuration.EnableLogs = true;
    configuration.ExceptionsEnabled = true;
});
```

## 14.5 分布式缓存使用

若要使用 `IDistributedCache` 接口，请 `IDistributedCache` 通过构造函数依赖关系注入。

```cs {5,16,30-33}
public class IndexModel : PageModel
{
    private readonly IDistributedCache _cache;

    public IndexModel(IDistributedCache cache)
    {
        _cache = cache;
    }

    public string CachedTimeUTC { get; set; }

    public async Task OnGetAsync()
    {
        CachedTimeUTC = "Cached Time Expired";
        // 获取分布式缓存
        var encodedCachedTimeUTC = await _cache.GetAsync("cachedTimeUTC");

        if (encodedCachedTimeUTC != null)
        {
            CachedTimeUTC = Encoding.UTF8.GetString(encodedCachedTimeUTC);
        }
    }

    public async Task<IActionResult> OnPostResetCachedTime()
    {
        var currentTimeUTC = DateTime.UtcNow.ToString();
        byte[] encodedCurrentTimeUTC = Encoding.UTF8.GetBytes(currentTimeUTC);

        // 设置分布式缓存
        var options = new DistributedCacheEntryOptions()
            .SetSlidingExpiration(TimeSpan.FromSeconds(20));

        await _cache.SetAsync("cachedTimeUTC", encodedCurrentTimeUTC, options);

        return RedirectToPage();
    }
}
```

## 14.6 分布式缓存建议

确定 `IDistributedCache` 最适合你的应用的实现时，请考虑以下事项：

- 现有基础结构
- 性能要求
- 成本
- 团队经验

缓存解决方案通常依赖于内存中的存储以快速检索缓存的数据，但是，内存是有限的资源，并且很昂贵。 仅将常用数据存储在缓存中。

通常，**`Redis` 缓存提供比 `SQL Server` 缓存更高的吞吐量和更低的延迟。** 但是，通常需要进行基准测试来确定缓存策略的性能特征。

当 `SQL Server` 用作分布式缓存后备存储时，对缓存使用同一数据库，并且应用的普通数据存储和检索会对这两种情况的性能产生负面影响。 建议使用分布式缓存后备存储的专用 `SQL Server` 实例。

## 14.7 反馈与建议

:::note 与我们交流

给 Furion 提 [Issue](https://gitee.com/dotnetchina/Furion/issues/new?issue)。

:::

---

:::note 了解更多

想了解更多 `分布式缓存` 知识可查阅 [ASP.NET Core - 分布式缓存](https://docs.microsoft.com/zh-cn/aspnet/core/performance/caching/distributed?view=aspnetcore-5.0) 章节。

:::
